package de.haw.vs.agent;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.ConnectException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

/**
 * Diese Klasse repraesentiert den MobilenAgenten der durch die Arbeitskontexte reist <br>
 * <br>
 * <b><font color='red'> Serial ID soll dynamisch von der JVM selbst automatisch generiert werden, daher soll die Klasse mit
 * 
 * @SuppressWarnings("serial") annotiert werden </font></b>
 * 
 * @author Maryna Maier
 */
@SuppressWarnings("serial")
public class MobileAgent implements Serializable {

	private static final int DEFAULT_WORKING_CONTEXT_PORT = 15000;

	private byte[] classByteArray;
	private String home;
	/**
	 * key ist die IP Adress, von der der Datensatz stammt<br>
	 * value ist der Datensatz selbst <br>
	 * Da uns die Reihenfolge, in der die Datensaetze in diese Map reinkommen, interessiert benutzen wir {@link LinkedHashMap}
	 */
	private final Map<String, String> tookWith = new LinkedHashMap<String, String>(); //Map mit Ergebnissen (zuordning Socket -> mitgenommener String)
	private final List<String> nextTargets = new ArrayList<String>(); //Liste mit den naechsten Zielen
	private final Set<String> passedTargets = new HashSet<String>(); //Liste mit schon besuchten Zielen, ausgeschaltete werden sofort eingetragen
	private String lastGotoDestination = "<unknown>"; 	//IP des letzten besuchten Arbeitskontextes
	private boolean goingHome = false;	//nach hause??
	private boolean silentMode = false;	//silent oder verbose

	/**
	 * Default Konstruktor. Er Muss existieren.
	 */
	public MobileAgent() {
		// no operation
	}

	/**
	 * Dieser Konstruktor erstellt die allererste Instanz des Mobilen Agenten
	 * 
	 * @param home
	 *            der Home Arbeitskontext
	 * 
	 * @param classByteArray
	 *            die byte Array der Klasse (ggf. der kompilierten .class Datei) fuer die spaetere dynamische Klassenladen
	 */
	public MobileAgent(String home, byte[] classByteArray) {
		this.home = home;
		this.classByteArray = classByteArray;
	}

	/**
	 * Liefert den Home Arbeitskontext
	 * 
	 * @return den Home Arbeitskontext
	 */
	private String getHome() {
		return home;
	}

	/**
	 * Die Ausfuehrungsmethode des Agenten
	 * 
	 * @param nextTargetsPath
	 *            der Dateiname mit den naechsten Zielen
	 * @param takeItWithPath
	 *            der Dateiname mit den Daten, die zu naechsten Zielen mitgnommen werden sollen
	 * @throws InterruptedException
	 *             bei Fehlern
	 */
	public void execute(String nextTargetsPath, String takeItWithPath) throws InterruptedException {
		//wenn Agent "nach Hause" geht
		if(goingHome) {
			//ergebnisse ausgeben
			presentateResultsAndDie();
		} else {
			//schreiben die IP:Port/mitgenommener Text in die Tabelle
			tookWith.put(lastGotoDestination, readNimmmit(takeItWithPath));
			//die ausgelesenen Sockets in die Liste hinzufuegen
			nextTargets.addAll(readNextTargets(nextTargetsPath));
			if(!silentMode) {
				System.out.println("Executed Test data: " + tookWith);
			}
			//um nicht zu schnell durch das Programm durchzulaufen
			Thread.sleep(1000);
			
			String nextTarget = null;

			boolean repeat = true;
			//endlos-schleife
			while (repeat) {				
				nextTarget = null;
				// solange nextTarget == null und  
				// die Liste mit den nicht besuchten Zielen nicht leer
				while (nextTarget == null && !nextTargets.isEmpty()) {
					//nextTarget setzen und aus der Liste rausloeschen
					nextTarget = nextTargets.remove(0);
					
					//wenn naechstes Ziel in der Liste schon besuchter Ziele enthalten ist
					if (passedTargets.contains(nextTarget)) {
						if(!silentMode) {
							System.out.println(nextTarget + " ist bereits besucht, nehme die Naechste...");
						}
						//...auf null setzen
						nextTarget = null;
					}
				}
				//falls naechstes Ziel == null
				if (nextTarget == null) {
					
					if(!silentMode) {
						System.out.println("Targets sind alle. Go Home....");
					}
					//...nach Hause schicken
					nextTarget = home;
					goingHome = true;
				}

				try {
					// geh zum naechsten Ziel
					goTo(nextTarget);
					repeat = false;
				} catch (ConnectException e) {
					// falls Ziel nicht erreichbar/nicht existiert
					System.out.println("Kann zu " + nextTarget + " nicht verbinden, probiere das naechste Ziel");
					repeat = true;
				}
			}
		}
	}

	/**
	 * LinkedHashMap parsen und die Daten ausgeben
	 */
	private void presentateResultsAndDie() {
		for(Entry<String, String> entry : tookWith.entrySet()) {
			String from = entry.getKey();
			String data = entry.getValue();
			
			System.out.println("Ich war im Arbeitskontext: " + from + " und habe Daten eigelesen: "  + data);
		}
		
		System.out.println("I'm dying");
		
	}
	
	/*
	 * liest die Datei "nimm_mit.txt" aus
	 */
	private String readNimmmit(String path) {
		StringBuilder nimmmit = new StringBuilder();

		try {
			BufferedReader br = new BufferedReader(new FileReader(path));
			try {
				for (String line = br.readLine(); line != null; line = br.readLine()) {
					if (nimmmit.length() > 0) {
						nimmmit.append("\n");
					}
					nimmmit.append(line);
				}
			} catch (IOException e) {
				nimmmit.append(e);
				e.printStackTrace();
			} finally {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		} catch (FileNotFoundException e) {
			nimmmit.append(e);
			e.printStackTrace();
		}

		return nimmmit.toString();
	}

	/*
	 * liest die Datei "next_targets.txt" aus
	 */
	private List<String> readNextTargets(String path) {
		List<String> targets = new ArrayList<String>();

		try {
			BufferedReader br = new BufferedReader(new FileReader(path));
			try {
				for (String line = br.readLine(); line != null; line = br.readLine()) {
					targets.add(line);
				}
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					br.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

		System.out.println("Eingelesen nextTargets: " + targets);
		return targets;
	}

	/**
	 * Reist zu naechter Station / ArbeitsContext
	 * 
	 * @param ip
	 *            die IP Adesse der naechsten Station
	 * @param port
	 *            die Portnummer der nachsten Station
	 * @throws ConnectException 
	 */
	private void goTo(String target) throws ConnectException {
		try {
			//aktuellen Knoten zu der Liste der besuchten Ziele hinzufuegen 
			passedTargets.add(target);
			
			//socket auf dem ':' auftrennen (10.0.1.12:22 --> 10.0.1.12 und 22)
			String[] ipPort = target.split("\\:");			
			String ip = ipPort[0];
			//erstmal den Port auf Standardwert setzen
			int port = DEFAULT_WORKING_CONTEXT_PORT;
			
			//wenn Port im Socket angegeben war
			if (ipPort.length == 2) {
				try {
					//versuche den ins Int zu parsen und zuweisen
					port = Integer.parseInt(ipPort[1]);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}

			if(!silentMode) {
				System.out.println("Goto: " + ip + ":" + port);
			}
			/*
			 * wird beim Einlesen von Nimmit Daten benoetigt. Siehe "execute"-Methode
			 */
			lastGotoDestination = ip + ":" + port;
			//Socket erzeugen
			Socket socket = new Socket(ip, port);
			OutputStream out = socket.getOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(out);
			//erzeugtes Object in den Stream schreiben 
			oos.writeObject(getTransferableData());
			//Versenden des Objects
			oos.flush();
			// Warte bis der Server/Arbeiskontext meldet, dass die Verbindung geschlossen werden kann
			socket.getInputStream().read();
			// Schliesse die Verbindung mit dem Server
			oos.close();
			socket.close();
		} catch(ConnectException conEx) {
			// Wird in execute Verwendet
			throw conEx;
		} catch (Throwable t) {
			t.printStackTrace();
		}

	}

	/**
	 * Erstellt aus der eigenen Instanz die Daten, die zum Versenden an den nachsten ArbeitsContext bereit sind
	 * 
	 * @return die Daten zum Versenden
	 * @throws IOException
	 *             bei Fehlern
	 */
	private Object[] getTransferableData() throws IOException {
		Object[] data = new Object[3];
		// Speichere den classen Namen
		data[0] = getClass().getName();
		// Speichere byte Array der Klasse
		data[1] = classByteArray; // Byte Array der .class Datei
		// Serialisiereung als byte array
		data[2] = getExternalization(); // Die spaetere Befuellung mit Daten
		return data;
	}

	/**
	 * Serialisiert die eigene Instanz
	 * 
	 * @return Byte Array mit Serialisierten Daten
	 * @throws IOException
	 *             bei Fehlern
	 */
	private byte[] getExternalization() throws IOException {

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(this);
		return baos.toByteArray();
	}

	/**
	 * Die Main Methode zum Anstossen des allerersten Agenten
	 * 
	 * @param args
	 * @throws ConnectException 
	 */
	public static void main(String[] args) throws ConnectException {

		String myHome = null;
		
		//Argumente einlesen
		for (String arg : args) {
			// home-Ziel faengt mit -h an
			if (arg.startsWith("-h")) {
				myHome = arg.substring(2);
			}
		}
		
		// wenn Home nicht vorhanden oder
		// home nicht als Socket aufgeschrieben
		if (myHome == null || !myHome.contains(":")) {
			throw new AssertionError(
					"Kein Home, oder keinen Port angegeben. Bitte Home korrekt als Argument uebergeben: -h<ip>:<port>, z.B. -h192.168.0.1:15000");
		}

		System.out.println("Home ist: " + myHome);
		//mobilen Agenten erzeugen 
		MobileAgent ma = new MobileAgent(myHome, ClassUtil.getmyClassBytes(MobileAgent.class));
		
		// JVM Parameter -Dsilent
		ma.silentMode = Boolean.getBoolean("silent");
		//zum ersten Mal nach Hause schicken
		ma.goTo(ma.getHome());
		System.out.println("Agent Main End");
	}
}
